# Copyright 2009 Larry Resnik, Lasfraka-Elixir

#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


import random
from math import pi
import pyglet
from pyglet.window import key
from physics import *

def set_graphic(obj, graphic, wasFacingRight=True):
    """Assign the passed graphic to obj's graphic based on facingRight.

    The graphic is flipped horizontally if obj is facing left or is
    facing right but turned around mid-frame (based on wasFacingRight).
    """

    if obj.facingRight:
        if wasFacingRight:
            # obj was facing right previously, so no flipping is needed.
            obj.graphic = graphic
        else:
            # If obj had stopped its animation and went to something else, the
            # frame would reset to 0. This makes turning around work correctly
            # if the state is already on frame 0. This becomes flawed when obj
            # needs to turn around mid-frame. obj's sprite is facing right but
            # still needs flipped. Hence the if, else statement this comment
            # resides in.
            obj.graphic = graphic.get_transform(flip_x=True)
    else:
        obj.graphic = graphic.get_transform(flip_x=True)
    center_anchor(obj.graphic)

def turn_around(obj):
    """Flip obj's graphic and invert its facingRight property."""
    wasFacingRight = obj.facingRight
    obj.facingRight = not obj.facingRight
    set_graphic(obj, obj.graphic, wasFacingRight)


class DeathCircle(object):
    """Class for the things that shoot out of the player upon dying.

    It's a bastardized BaseBullet.
    """

    collideType = COLLIDE_TYPE_NONE
    image = None
    dead = False
    tolerance = 32

    def __init__(self, manager, id, x, y, speed=1, angle=0.0):
        if DeathCircle.image is None:
            DeathCircle.image = pyglet.resource.image('camera_center.png')
            center_anchor(DeathCircle.image)
        self.manager = manager
        self.id = id
        self.x = x
        self.y = y
        self.speed = speed
        self.rotation = angle
        self.direction = Vector2D()
        self.direction.set_direction(-self.rotation)
        self.dx = self.speed * self.direction.x
        self.dy = self.speed * self.direction.y

    def remove(self):
        del self.manager.currentScene.objectInstances[self.id]

    def update(self, offsetX, offsetY, aimable, level):
        self.x += self.dx
        self.y += self.dy
        if self.x < offsetX - self.tolerance or \
            self.x > offsetX + 320 + self.tolerance or \
            self.y < offsetY - 64 - self.tolerance or \
            self.y > offsetY + 240 + self.tolerance:
            self.dead = True

    def draw(self):
        self.image.blit(self.x, self.y)

class Thing(object):
    """This can be a placeholder object for anything with an x/y."""

    collideType = COLLIDE_TYPE_RECT

    def __init__(self, manager, bulletHandler):
        self.manager = manager
        self.bulletHandler = bulletHandler
        self.dead = False
        self.facingRight = True
        self.health = 8
        self.xSpeed = 0
        self.ySpeed = 0
        # All Things can deal damage upon impact. But 0 damage is ignorable.
        self.damage = 0
        self.currentFrame = 0
        self.state = None
        self.previousState = None
        # This tracks the last grid index used for the thing's graphic. If it
        # differs from a formula's new image index, the image is changed.
        self.previousImage = 0
        # I store this because I don't know how to derive from TextureRegion..
        self.graphic = None
        # This is a counter for enabling the effects of gravity.
        self.gravityPause = 0

    def hit(self, damager):
        """Inflicts the effects of colliding with the damager.

        Unless this Thing is invincible, it should always call the damager's
        post_collide method. Most Things will need to override this to interact
        with damage and health.
        """
        damager.post_collide(self)

    def post_collide(self, damaged_obj):
        """Make this object react to having collided with something

        Most objects will be content doing nothing with this.
        """
        pass

    def remove(self):
        """Removes this Thing from the owner's array of objects."""
        del self.manager.currentScene.objectInstances[self.id]

    def draw(self):
        pass

class Player(Thing):
    """This is a class made to be subclassed to make a player."""

    # baseStateList holds states shared between all players.
    baseStateList = set(['idle', 'stand', 'duck', 'walk', 'jump', 'wallgrip',
                     'wallkick', 'ladder', 'hit', 'dash', 'airdash', 'death'])

    def __init__(self, manager, bulletHandler):
        super(Player, self).__init__(manager, bulletHandler)
        self.maxHealth = self.health
        # This is the time the player held the jump button to go higher.
        self.ascentTime = 0
        # This time is how long the player is safe after being hit.
        self.invincibleTime = 0
        # More literally this is the speed for not dashing.
        self.walkSpeed = 3
        self.dashSpeed = 5
        # This is the speed for beginning a jump.
        self.jumpSpeed = 5
        # This is how long the jump button may be held to prolong a jump.
        self.MAX_ASCENT = 20
        self.canAirDash = True
        self.grazeCounter = 0

    def revive(self):
        """Regenerate the player to begin the level anew."""
        self.health = self.maxHealth
        self.dashTime = 0
        self.ascentTime = 0
        self.grazeCounter = 0
        self.state = 'idle'
        self.collideType = COLLIDE_TYPE_RECT

class Marisa(Player):
    def __init__(self, manager, bulletHandler, x=0, y=0):
        super(Marisa, self).__init__(manager, bulletHandler)
        stateList = set(Player.baseStateList)

        self.debugText = None
        # This variable tracks the player's state name when it's assigned.
        # It's only purpose is to watch the player's state in real time.
        self.stateString = None
        self.debugFreeze = False

        self.width = 20
        self.height = 28
        self.halfWidth = int(self.width / 2)
        self.halfHeight = int(self.height / 2)
        self.x = x + self.halfWidth
        self.y = y + self.halfHeight
        self.xSpeed = 0
        self.ySpeed = 0
        self.radius = 4
        self.set_state('idle')
        self.previousState = self.state
        # This is whether or not the charging aura is visible or not.
        self.aura = True
        # This is a frame counter for how long the shot button was held.
        self.heldShot = 0
        # This is how long the player must wait before being able to shoot
        # subsequent normal shots
        self.SHOT_COOLDOWN = 1
        # This is a frame counter for how long the dash button was held.
        self.dashTime = 0
        # This is how long the dash button may enable a dash.
        self.MAX_DASH = 30
        # This is a debugging variable to show the static aura.
        self.lookAtari = False
        # I need these to communicate easily between frames and actions.
        # This tells when the wallkick's 1st frame of animation ends.
        self.firstWallkickFrameEnd = 3
        self.firstDashFrameEnd = 7

        self._init_sprites()

    def _init_sprites(self):
        self.standGraphic = pyglet.resource.image('stand.png')
        self.hitGraphic = pyglet.resource.image('hit.png')
        walk = pyglet.resource.image('walk.png')
        walk = pyglet.image.ImageGrid(walk, 1, 11)
        self.walkGrid = pyglet.image.TextureGrid(walk)
        jump = pyglet.resource.image('jump.png')
        jump = pyglet.image.ImageGrid(jump, 1, 7)
        self.jumpGrid = pyglet.image.TextureGrid(jump)
        intro = pyglet.resource.image('intro.png')
        intro = pyglet.image.ImageGrid(intro, 1, 6)
        self.introGrid = pyglet.image.TextureGrid(intro)
        self.wallgripImg = pyglet.resource.image('wallgrip.png')
        wallkick = pyglet.resource.image('wallkick.png')
        wallkick = pyglet.image.ImageGrid(wallkick, 1, 2)
        self.wallkickGrid = pyglet.image.TextureGrid(wallkick)
        dash = pyglet.resource.image('dash.png')
        dash = pyglet.image.ImageGrid(dash, 1, 2)
        self.dashGrid = pyglet.image.TextureGrid(dash)
        set_graphic(self, self.standGraphic)

        self._init_aura()
        self.auraSprite.opacity = self.auraSpriteSpinner.opacity = 64

    def _init_aura(self):
        self.baseBackupImg = pyglet.resource.image('14x28.png')
        center_anchor(self.baseBackupImg)
        self.auraImage = pyglet.resource.image('28x28.png')
        center_anchor(self.auraImage)
        self.auraSpriteSpinner = pyglet.sprite.Sprite(self.auraImage,
            x=160, y=120)
        self.auraSprite = pyglet.sprite.Sprite(self.baseBackupImg, x=160,y=120)
        self.auraTorque = 5.0

    def tile_react(self, significant_tile, level):
        """Reacts to unique tiles."""
        if significant_tile == TPROP_DAMAGE:
            self.hit(level)
        elif significant_tile == TPROP_LETHAL:
            self.hit(level)

    def set_state(self, state):
        if state != self.state:
            self.currentFrame = -1
            self.previousState = self.state
            self.state = state
        if self.debugText:
            self.stateString = state

    def hit(self, damager):
        if not self.invincibleTime:
            gotHit = False
            if damager.collideType == COLLIDE_TYPE_CIRC:
                # Assume a bullet.
                if damager.damage > 0 and not damager.grazed:
                    # Attempt to graze.
                    if self.state in ('dash', 'airdash'):
                        damager.grazed = True
                        self.grazeCounter += 1
                    else:
#                        print('graze', self.grazeCounter)
                    # Now test if the bullet hit the collision circle.
                        gotHit = circ_circtuple_collide(damager,
                            (self.x, self.y, self.radius))
            if gotHit or damager.collideType == COLLIDE_TYPE_RECT:
                # Could be an enemy, damaging tile, item, etc.
                if damager.damage > 0:
                    self.set_state('hit')
                    # This will prevent being hit multiple times in the same
                    # update cycle.
                    self.invincibleTime = 1
                self.health -= damager.damage
                if self.health <= 0 and not \
                    self.manager.settings.properties['invincibility']:
                    self.set_state('death')
                    self.health = 0
                    self.collideType = COLLIDE_TYPE_NONE
                damager.post_collide(self)

    def _update_typical(self, level, keys):
        """Do one update cycle assuming the object's state is mundane."""

        directionX = 0
        # While moving left/right, did we run into a thick tile?
        # It doesn't matter if we are already standing still.
        movedTileEncounter = TPROP_THICK
        # TODO: See if altering sig(ry down the line can clobber more
        # significant tiles.
        significantTile = TPROP_EMPTY - 1

        if keys.left.held:
            directionX -= 1
        if keys.right.held:
            directionX += 1
        if keys.dash.held:
            if not self.dashTime and (self.state in ('walk', 'stand')):
                self.set_state('dash')
            if self.state == 'jump':
                if self.canAirDash and not(self.dashTime or \
                    self.state == 'airdash'):
                    self.set_state('airdash')
                    self.ySpeed = 0
                    self.ascentTime = self.MAX_ASCENT + 100
                    self.dashTime = 0
        else:
            if self.state == 'dash':
                self.set_state('stand')
            if self.state == 'airdash':
                # Stopped holding dash during airdash. Don't reallow dashing.
                self.dashTime = self.MAX_DASH + 100
            elif self.state != 'jump':
                # Could've held dash in the air. Don't reset in that case.
                self.dashTime = 0

        if keys.jump.held:
            # It won't work if jump was held down.
            if not (self.ascentTime) and \
                not(self.state in ('jump', 'wallkick', 'wallgrip', 'airdash')):
                self.ySpeed = self.jumpSpeed
                self.ascentTime = 0
                self.set_state('jump')
                self.manager.play_sound('beep_1.mp3')
            # The hard-coded 15 is how long your ascent can be constant.
            if self.state in ('jump', 'wallkick'):
                if (self.ySpeed == self.jumpSpeed or \
                    self.ySpeed == self.jumpSpeed - 1) and \
                    self.ascentTime <= self.MAX_ASCENT:
                    # Offset gravity.
                    self.ySpeed = self.jumpSpeed
                    self.ascentTime += 1
                else:
                    # Held "jump" during a fall. Prevents jumping upon landing.
                    self.ascentTime = self.MAX_ASCENT + 100
        else:
            # Allow jumping at (almost) any time.
            if self.state != 'jump':
                self.ascentTime = 0

        if keys.shoot.held:
            self.heldShot += 1
            if self.heldShot > 20:
                self.aura = True
            if self.heldShot > 50:
                self.auraTorque = 15.0
        else:
            if self.heldShot >= self.SHOT_COOLDOWN and self.bulletHandler.count < 3:
                if self.facingRight:
                    shot_x = self.x + self.width
                    shot_speed = 4.0
                else:
                    shot_x = self.x - self.width
                    shot_speed = -4.0
                if self.heldShot < 25:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
                elif self.heldShot < 70:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
                else:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
            self.heldShot = 0
            self.auraSpriteSpinner.rotation = 0.0
            self.aura = False
            self.auraTorque = 5.0

        if self.state == 'jump':
            previousSpeed = self.ySpeed
            significantTile = jump(self, level)
            if significantTile >= TPROP_THICK and previousSpeed <= 0:
                # During descent, we landed.
                self.set_state('stand')
                # Disallow dashing from the moment we hit the ground.
                self.dashTime = self.MAX_DASH + 100
            elif self.ySpeed == 0 and previousSpeed > 1:
                # During ascent, we hit a ceiling.
                self.manager.play_sound('click.wav')
                self.ascentTime = self.MAX_ASCENT + 100

        if self.state == 'dash':
            self.dashTime += 1
            if self.dashTime >= self.MAX_DASH:
                self.set_state('stand')
            elif (self.facingRight and directionX < 0) or \
                (not self.facingRight and directionX > 0):
                # Player turned around during a dash. Stop dashing.
                self.set_state('walk')
                self.xSpeed = self.walkSpeed
                self.dashTime = self.MAX_DASH + 100
            else:
                if self.currentFrame >= self.firstDashFrameEnd:
                    # Force the player to go in that direction.
                    self.xSpeed = self.dashSpeed
                elif not directionX:
                    # Player held dash but not a directional key.
                    self.xSpeed = 1
                # Enforce movement.
                if self.facingRight:
                    directionX = 1
                else:
                    directionX = -1

        if self.state == 'airdash':
            self.dashTime += 1
            # Offset gravity
            self.ySpeed = 0
            if self.dashTime >= self.MAX_DASH:
                self.ySpeed = 0
                self.set_state('jump')
            elif (self.facingRight and directionX < 0) or \
                (not self.facingRight and directionX > 0):
                # Player turned around during a dash. Stop dashing.
                self.set_state('jump')
                self.xSpeed = self.walkSpeed
                self.ySpeed = 0
                self.dashTime = self.MAX_DASH + 100
            else:
                self.xSpeed = self.dashSpeed
                # Enforce movement.
                if self.facingRight:
                    directionX = 1
                else:
                    directionX = -1

        # Assign xSpeed.
        if self.state == 'wallkick':
            # Deflag significantTile-not-checked statement at the end.
            significantTile = TPROP_EMPTY
            if self.currentFrame >= self.firstWallkickFrameEnd:
                if self.dashTime:
                    self.xSpeed = self.dashSpeed
                else:
                    self.xSpeed = self.walkSpeed
            else:
                # Override the left/right keys.
                directionX = 0
                self.xSpeed = 0
            if self.xSpeed:
                self.ySpeed = self.jumpSpeed
                # Override the left/right keys and move backwards.
                # xSpeed will be reversed for us.
                if self.facingRight:
                    directionX = -1
                else:
                    directionX = 1
                # We only need to "jump" after getting off of the wall.
                significantTile = jump(self, level)
                if significantTile >= TPROP_THICK:
                    # We hit a ceiling.
                    self.set_state('jump')
                    self.ascentTime = self.MAX_ASCENT + 100
                    self.manager.play_sound('click.wav')
        else:
            if self.state == 'jump':
                if directionX:
                    if self.dashTime > 0 and self.dashTime < self.MAX_DASH:
                        self.xSpeed = self.dashSpeed
                    else:
                        self.xSpeed = self.walkSpeed
                else:
                    self.xSpeed = 0
            elif self.state not in ('dash', 'airdash'):
                if directionX:
                    self.xSpeed = self.walkSpeed
                else:
                    self.xSpeed = 0
            # Our direction may have changed mid-frame.
            # We don't want to turn around during a wall-kick.
            if (directionX < 0 and self.facingRight) or \
                (directionX > 0 and not self.facingRight):
                turn_around(self)

        #TODO: Apply wind here?

        if directionX:
            movedTileEncounter = move_horizontal(self, directionX, self.xSpeed,
                level)
            significantTile = max(movedTileEncounter, significantTile)
            if movedTileEncounter < TPROP_THICK and (
                self.state in ('walk', 'stand')):
#                print('began/continued walking')
                self.set_state('walk')
            # We may have walked off of thick land.
            if self.state not in ('jump', 'wallgrip', 'wallkick', 'airdash'):
                significantTile = fall(self, level)
                if significantTile < TPROP_THICK:
                    # We fell off a cliff.
                    self.set_state('jump')
                    self.ascentTime = self.MAX_ASCENT + 100
            # We don't want to re-turn-around because of the wall-grip, so
            # this code is after the turn-around check.
            if movedTileEncounter >= TPROP_THICK and \
                self.ySpeed < 0 and self.state == 'jump':
                self.set_state('wallgrip')
                self.facingRight = not self.facingRight

        if movedTileEncounter >= TPROP_THICK and \
            (self.state in ('walk', 'dash')):
            # We walked into something thick or stopped moving.
#            print('ran into something')
            self.set_state('stand')
        if movedTileEncounter >= TPROP_THICK and self.state == 'airdash':
            # We airdashed into something thick.
            self.set_state('jump')
            self.ySpeed = 0
        if significantTile == TPROP_EMPTY - 1:
            # The thing was never set; the player probably didn't move.
            # Check under our feet.
            significantTile = fall(self, level)
            if significantTile < TPROP_THICK:
#                print('test fall')
                # A disappearing tile just disappeared.
                self.set_state('jump')
                self.ascentTime = self.MAX_ASCENT + 100
        self.tile_react(significantTile, level)

    def _update_for_hit(self, level, keys):
        """Do one update cycle assuming the object is in a "hit" state."""

        self.ascentTime = self.MAX_ASCENT + 100
        self.dashTime = self.MAX_DASH + 100

        if self.facingRight:
            directionX = 1
        else:
            directionX = -1
        self.xSpeed = int(self.walkSpeed / 3 * 2)

        # Fall if not on land.
        self.tile_react(jump(self, level), level)

        if keys.shoot.held:
            # Allow charging.
            self.heldShot += 1
            if self.heldShot > 20:
                self.aura = True
            if self.heldShot > 50:
                self.auraTorque = 15.0
        else:
            # Don't allow shooting.
            self.heldShot = 0
            self.auraSpriteSpinner.rotation = 0.0
            self.aura = False
            self.auraTorque = 5.0

        if directionX:
            self.tile_react(move_horizontal(self, directionX, self.xSpeed,
                level), level)

    def _update_for_wallgrip(self, level, keys):
        """Do one update cycle assuming the object's state is a wall-grip."""

        directionX = 0
        significantTile = TPROP_EMPTY - 1

        if keys.left.held:
            directionX -= 1
        if keys.right.held:
            directionX += 1
        if keys.dash.held:
            # This allows dashing from the wall. Beyond the max won't allow
            # a dash during a jump.
            self.dashTime = 1
        else:
            self.dashTime = 0
        if keys.jump.held:
            # It won't work if jump was held down.
            if not (self.ascentTime or self.state == 'jump'):
                self.ascentTime = 0
                self.set_state('wallkick')
                self.manager.play_sound('beep_1.mp3')
            else:
                # Don't let the player jump in case we touch land.
                self.ascentTime = self.MAX_ASCENT + 100
        else:
            # Allow jumping at any time.
            self.ascentTime = 0

        if keys.shoot.held:
            self.heldShot += 1
            if self.heldShot > 20:
                self.aura = True
            if self.heldShot > 50:
                self.auraTorque = 15.0
        else:
            if self.heldShot >= self.SHOT_COOLDOWN and self.bulletHandler.count < 3:
                if self.facingRight:
                    shot_x = self.x + self.width
                    shot_speed = 4.0
                else:
                    shot_x = self.x - self.width
                    shot_speed = -4.0
                if self.heldShot < 25:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
                elif self.heldShot < 70:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
                else:
                    self.bulletHandler.shoot('marisa1', x=shot_x, y=self.y,
                        speed=shot_speed)
            self.heldShot = 0
            self.auraSpriteSpinner.rotation = 0.0
            self.aura = False
            self.auraTorque = 5.0

        significantTile = move_horizontal(self, directionX, self.xSpeed, level)
        if significantTile < TPROP_THICK or \
            significantTile == TPROP_NO_WALLGRIP:
            # We slid off of the bottom of a (floating) wall or stopped
            # pressing the direction towards the wall.
            self.set_state('jump')
            # Don't allow jumping from the air.
            self.ascentTime = self.MAX_ASCENT + 100
            self.ySpeed = 0
        self.tile_react(significantTile, level)
        significantTile = moveVertical(self, -1, level)
        if significantTile >= TPROP_THICK:
            # We slid down onto land.
            self.set_state('stand')
            self.dashTime = self.MAX_DASH + 100

        if significantTile == TPROP_EMPTY - 1:
            # The thing was never set. Check under our feet.
            significantTile = fall(self, level)
        self.tile_react(significantTile, level)

    def _update_for_death(self, level, keys):
        """Do one update cycle assuming the object is in a "hit" state."""
        self.invincibleTime = 3
        self.aura = False

    def update(self, level, keys):
        """Progress this object one frame through the game.

        Particular states may have different function calls. This function
        acts out the common tasks shared by all states.
        """

        if self.debugFreeze:
            return
        if self.debugText:
            self.debugText.text = ''

        if self.state == 'hit':
            self._update_for_hit(level, keys)
        elif self.state == 'wallgrip':
            self._update_for_wallgrip(level, keys)
        elif self.state == 'death':
            self._update_for_death(level, keys)
        else:
            self._update_typical(level, keys)

        self.invincibleTime = max(self.invincibleTime - 1, 0)
        self.auraSpriteSpinner.rotation += self.auraTorque
        if self.auraSpriteSpinner.rotation >= 360.0:
            self.auraSpriteSpinner.rotation -= 360.0
        self._update_frame(level)
        if self.debugText:
            self.debugText.text = 'Frame: %s %s' % \
                (self.stateString, self.currentFrame)
            #self.debugText.text = 'Shot Time: %s' % self.heldShot
            #self.debugText.text = 'Jump Time: %s' % self.ascentTime
            #self.debugText.text = 'Dash Time: %s' % self.dashTime
            #t, b, l, r = calculateBorders(self.x, self.y, self)
            #self.debugText.text = 'Left %i, Bottom %i' % (l, b)

    def _update_frame(self, level):
        self.currentFrame += 1

        if self.state == 'jump':
            if self.ySpeed > 0:
                # images = 7
                if self.currentFrame >= 2*4: # framespeed = 4; image = 2
                    self.currentFrame = 3*4-1
                    if self.previousImage != 2:
                        set_graphic(self, self.jumpGrid[2])
                        self.previousImage = 2
                else:
                    newImage = int(self.currentFrame / 4)
                    if newImage != self.previousImage:
                        set_graphic(self, self.jumpGrid[newImage])
                        self.previousImage = newImage
            else:
                if self.currentFrame < 3*4-1:
                    # We hit the ceiling or started falling.
                    self.currentFrame = 3*4-1
                if self.currentFrame >= 4*4:
                    self.currentFrame = 5*4-1
                    if self.previousImage != 4:
                        set_graphic(self, self.jumpGrid[4])
                        self.previousImage = 4
                else:
                    if self.previousImage != 3:
                        set_graphic(self, self.jumpGrid[3])
                        self.previousImage = 3
        elif self.state == 'dash':
            if self.currentFrame <= self.firstDashFrameEnd:
                if self.previousImage != 0:
                    set_graphic(self, self.dashGrid[0])
                    self.previousImage = 0
            else:
                if self.previousImage != 1:
                    set_graphic(self, self.dashGrid[1])
                    self.previousImage = 1
        elif self.state == 'airdash':
            if self.previousImage != 6:
                set_graphic(self, self.dashGrid[1])
                self.previousImage = 6
        elif self.state == 'wallkick':
            if self.currentFrame <= self.firstWallkickFrameEnd:
                if self.previousImage != 0:
                    # Turn around the 1st time.
                    self.facingRight = not self.facingRight
                    set_graphic(self, self.wallkickGrid[0])
                    self.previousImage = 0
            elif self.currentFrame <= self.firstWallkickFrameEnd + \
                int(self.MAX_ASCENT / 2):
                self.ascentTime = self.currentFrame-self.firstWallkickFrameEnd
                if self.previousImage != 1:
                    set_graphic(self, self.wallkickGrid[1])
                    self.previousImage = 1
            else:
                self.set_state('jump')
                # Make the current frame the "rising" animation.
                self.currentFrame = 2*4
        elif self.state == 'walk':
            if self.currentFrame >= 44: #11*4
                self.currentFrame = 0
            newImage = int(self.currentFrame / 4)
            if newImage != self.previousImage:
                set_graphic(self, self.walkGrid[newImage])
                self.previousImage = newImage
        elif self.state == 'stand':
            if self.previousImage != -4:
                set_graphic(self, self.standGraphic)
                self.previousImage = -4
        elif self.state == 'hit':
            if self.previousImage != -2:
                self.invincibleTime = 80
                # Set the graphic but don't let it get flipped horizontally.
                set_graphic(self, self.hitGraphic)
                # Now invert the x speed/direction.
                self.facingRight = not self.facingRight
                self.previousImage = -2
            if self.currentFrame >= 30:
                # Re-invert the x speed/direction; the graphic is already
                # facing the right direction.
                self.facingRight = not self.facingRight
                previousSpeed = self.ySpeed
                if fall(self, level) < TPROP_THICK:
                    # We got out of the hit animation in the air. Undo the
                    # fall function's effect of neutralizing the ySpeed.
                    self.ySpeed = previousSpeed
                    self.set_state('jump')
                else:
                    self.set_state('stand')
        elif self.state == 'idle':
            if self.previousImage != -3:
                set_graphic(self, self.introGrid[0])
                self.previousImage = -3
        elif self.state == 'wallgrip':
            if self.previousImage != -5:
                set_graphic(self, self.wallgripImg)
                self.previousImage = -5
            # TODO: Move this to _update_for_wallgrip?
            if self.currentFrame <= 5:
                self.ySpeed = 0
            else:
                self.ySpeed = -2
        elif self.state == 'death':
            if self.previousImage != -7:
                set_graphic(self, self.hitGraphic)
                self.previousImage = -7
        else:
            if self.previousImage != -10:
                set_graphic(self, self.baseBackupImg)
                self.previousImage = -10
        # TODO: Base the hitbox on the STATE.

    def draw(self):
        # This formula makes the player flash 2 out of every 3 frames while
        # invincible. It does not take effect when not invincible.
        if not self.invincibleTime % 3:
            # (centerX=10*32, centerY=8*32)
            self.graphic.blit(160, 120)
        if self.lookAtari:
            self.auraSprite.draw()
        if self.aura:
            self.auraSpriteSpinner.draw()

class Arwing(Player):
    """This is a free-moving player used for debugging."""
    def __init__(self, manager, bulletHandler, x=0, y=0):
        super(Arwing, self).__init__(manager, bulletHandler)
        self.state = 'idle'
        self.width = 32
        self.height = 32
        self.halfWidth = int(self.width / 2)
        self.halfHeight = int(self.height / 2)
        self.x = x * 32 + self.halfWidth
        self.y = y * 32 + self.halfHeight
        self.xSpeed = self.walkSpeed
        self.ySpeed = self.walkSpeed
        self.radius = 4
        self.pressed = False
        self.debugFreeze = False
        set_graphic(self, pyglet.resource.image('player.png'))

    def hit(self, damager):
        gotHit = False
        if damager.collideType == COLLIDE_TYPE_CIRC:
            # Assume a bullet.
            if damager.damage > 0:
                # Attempt to graze.
                if not damager.grazed:
                    damager.grazed = True
                    self.grazeCounter += 1
#                    print('graze', self.grazeCounter)
                # Now test if the bullet hit the collision circle.
                gotHit = circ_circtuple_collide(damager,
                        (self.x, self.y, self.radius))
        if gotHit or damager.collideType == COLLIDE_TYPE_RECT:
            # Could be an enemy, damaging tile, item, etc.
            damager.post_collide(self)

    def set_debug_text(self, text=None):
        pass

    def set_state(self, state=None):
        pass

    def update(self, level, keys):
        if self.debugFreeze:
            return
        directionX = 0
        directionY = 0
#        if not (keyStates[key.MINUS] or keyStates[key.EQUAL]):
#            self.pressed = False
#        if keyStates[key.MINUS] and not self.pressed:
#            self.pressed = True
#            self.xSpeed = max(self.xSpeed - 1, 0)
#            self.ySpeed = max(abs(self.ySpeed) - 1, 0)
#        if keyStates[key.EQUAL] and not self.pressed:
#            self.pressed = True
#            self.xSpeed = min(self.xSpeed + 1, 31)
#            self.ySpeed = min(abs(self.ySpeed) + 1, 31)
        if keys.left.held:
            directionX -= 1
        if keys.right.held:
            directionX += 1
        if keys.down.held:
            directionY -= 1
        if keys.up.held:
            directionY += 1
        # Diagonal overrides. Good for precise tile collision.
        if keys.special.held:
            # Up-right
            directionX = 1
            directionY = 1
        if keys.dash.held:
            # Down-right
            directionX = 1
            directionY = -1
        if keys.jump.held:
            # Down-left
            directionX = -1
            directionY = -1
        if keys.shoot.held:
            # Up-left is not available.
            self.bulletHandler.shoot('marisa1', x=self.x, y=self.y,
                speed=4.0, angle=random.random()*360)

        if directionX:
            move_horizontal(self, directionX, self.xSpeed, level)
        if directionY:
            if directionY > 0:
                self.ySpeed = abs(self.ySpeed)
            else:
                self.ySpeed = -abs(self.ySpeed)
            moveVertical(self, directionY, level)

    def draw(self):
        self.graphic.blit(160, 120)

class Metool(Thing):
    """A primitive enemy which sits idly until the player gets close."""

    spritesheet = None
    tolerance = 64
#    stateList = set(('wait', 'walk', 'getup', 'sitdown'))
    width = 22
    height = 21
    halfWidth = int(width / 2)
    halfHeight = int(height / 2)

    def __init__(self, manager, bulletHandler, id, x=0, y=0, batch=None):
        if Metool.spritesheet is None:
            image = pyglet.resource.image('metool.png')
            grid = pyglet.image.ImageGrid(image, 1, 8)
            Metool.spritesheet = pyglet.image.TextureGrid(grid)
        # TODO: Use the batch.
        super(Metool, self).__init__(manager, bulletHandler)
        self.damage = 2
        self.id = id
        self.x = x + Metool.halfWidth
        self.y = y + Metool.halfHeight
        self.health = 5
        self.xSpeed = 1
        self.ySpeed = -1
        self.radius = 10
        self.state = 'wait'
        self.previousState = self.state
        self.airial = False
        self.walkTime = 0
        self.waitTime = 0
        # This is how long to stay invisible after being hit.
        self.flickerTime = 0
        set_graphic(self, Metool.spritesheet[0])

    def set_state(self, state):
        self.state = state

    def shoot(self, aimable):
        if self.facingRight:
            ang = 0
        else:
            ang = pi / 2.0
        bull = 'small'
        y = self.y + self.halfHeight
        self.bulletHandler.shoot(bull, self.x, y, speed=3.5, angle=ang)
        self.bulletHandler.shoot(bull, self.x, y, speed=3.0, angle=ang+pi/1.5)
        self.bulletHandler.shoot(bull, self.x, y, speed=3.0, angle=ang+pi/3.0)
        aimedBullet = self.bulletHandler.shoot(bull, self.x, y, speed=2.0)
        aimedBullet.point_to(aimable['p'])

    def hit(self, damager):
        if self.state == 'wait':
            # Reflect the bullet.
            pass
        elif damager.damage:
            if not self.flickerTime:
                self.flickerTime = 3
            self.health -= damager.damage
            if self.health <= 0:
                self.dead = True
        damager.post_collide(self)

    def _update_frame(self, aimable):
        self.currentFrame += 1
        if self.previousState != self.state:
            self.currentFrame = 0
            self.previousState = self.state

        if self.state == 'walk':
            if self.currentFrame >= 32: # 8*4
                self.currentFrame = 0
            # Offset by 5 pictures.
            newImage = int(self.currentFrame / 8) + 4
            if newImage != self.previousImage:
                set_graphic(self, Metool.spritesheet[newImage])
                self.previousImage = newImage
        elif self.state == 'getup':
            if self.currentFrame >= 8*4:
                self.shoot(aimable)
                # Change state.
                self.state = 'walk'
                self._update_frame(aimable)
                # Frame was just updated earlier, so stop processing.
                return
            # Offset by 1 picture.
            newImage = int(self.currentFrame / 8) + 0
            if newImage != self.previousImage:
                set_graphic(self, Metool.spritesheet[newImage])
                self.previousImage = newImage
        elif self.state == 'sitdown':
            if self.currentFrame >= 8*3:
                # Change state.
                self.state = 'wait'
                self._update_frame(aimable)
                self.waitTime = 0
                return
            # This formula reverses the displayed pictures from 3 to 1.
            newImage = int(self.currentFrame / 8) + 3 - 2 * \
                int(self.currentFrame / 8)
            if newImage != self.previousImage:
                set_graphic(self, Metool.spritesheet[newImage])
                self.previousImage = newImage
        else:
            # state is 'wait'.
            set_graphic(self, Metool.spritesheet[0])

    def update(self, offsetX, offsetY, aimable, level):
        self.flickerTime = max(self.flickerTime - 1, 0)
        self._update_frame(aimable)
        if self.state == 'wait':
            self.waitTime += 1
            # See if the player is close to the Metool.
            if self.waitTime >= 40 and \
                point_in_rect(aimable['p'], self.x - 125, self.x + 125,
                              self.y + 100, self.y - 100):
                if aimable['p'].x <= self.x:
                    self.facingRight = False
                else:
                    self.facingRight = True
                self.xSpeed = abs(self.xSpeed)
                self.state = 'getup'
        elif self.state == 'walk':
            self.walkTime += 1
            if move_horizontal(self, 1 if self.facingRight else -1,
                self.xSpeed, level) >= TPROP_THICK:
                turn_around(self)
            if self.walkTime >= 60:
                self.state = 'sitdown'
                # Reset walkTime for the future use.
                self.walkTime = 0
        if not self.airial and fall(self, level) < TPROP_THICK:
            # We fell off of the land.
            self.airial = True
        if self.airial and jump(self, level) >= TPROP_THICK:
            # We landed on something.
            self.airial = False
        if self.x < offsetX - self.tolerance or \
            self.x > offsetX + 320 + self.tolerance or \
            self.y < offsetY - 64 - self.tolerance or \
            self.y > offsetY + 240 + self.tolerance:
            self.dead = True

    def draw(self):
        if self.flickerTime == 0:
            self.graphic.blit(self.x, self.y)
